﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Metadata;
using System.Web.Services.Protocols;
using System.Xml;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Query;

namespace NetPlanetes.Crm.ManyToManyRelationshipPlugin
{
    /// <summary>
    /// This plugin associate or disassociate entity records when np_manytomanyrelationship
    /// record is create from entity form or by using import.
    /// np_relationshipname is many to many relationship name
    /// np_entity1 is primary field value of one records
    /// np_entity2 is primary field value of another records to associate.
    /// np_associationtype is type of association.
    /// 
    /// if plugin fail, try to replace entity1, entity2 value.
    /// </summary>
    public class ManyToManyRelationshipPlugin : IPlugin
    {
        #region constant property
        private static readonly string Np_relationshipname = "np_relationshipname";
        private static readonly string Np_entity1 = "np_entity1";
        private static readonly string Np_entity2 = "np_entity2";
        private static readonly string Np_remarks = "np_remarks";
        private static readonly string Np_associationtype = "np_associationtype";
        private static readonly int Associate = 1;
        private static readonly int Disassociate = 2;
        #endregion

        #region IPlugin Members
        /// <summary>
        /// Plugin Execution Entry
        /// </summary>
        /// <param name="context"></param>
        public void Execute(IPluginExecutionContext context)
        {
            try
            {
                ValidateContext(context);           // check context
                ProcessAssociateEntities(context);  // make association
            }
            catch (Exception e)
            {
                if (e is InvalidPluginExecutionException) throw;
                else throw new InvalidPluginExecutionException(e.Message, e);
            }
        }
        #endregion

        /// <summary>
        /// Set or Clear Association
        /// </summary>
        /// <param name="context"></param>
        private void ProcessAssociateEntities(IPluginExecutionContext context)
        {
            DynamicEntity entity = context.InputParameters[ParameterName.Target] as DynamicEntity;
            string relationshipname = entity.Properties[ManyToManyRelationshipPlugin.Np_relationshipname] as string;
            string entity1 = entity.Properties[ManyToManyRelationshipPlugin.Np_entity1] as string;
            string entity2 = entity.Properties[ManyToManyRelationshipPlugin.Np_entity2] as string;
            Picklist associationType = entity.Properties[ManyToManyRelationshipPlugin.Np_associationtype] as Picklist;
            
            try
            {
                // Retrieve many to many relationsihp metadata
                IMetadataService metaService = context.CreateMetadataService(true);
                ManyToManyMetadata manyToManyMetadata = GetManyToManyMetadata(metaService, relationshipname);

                // Retrieve entity1 guid and entity2 guid
                ICrmService crmService = context.CreateCrmService(true);
                Guid entity1id = GetEntityId(metaService, crmService, manyToManyMetadata.Entity1LogicalName, entity1);
                Guid entity2id = GetEntityId(metaService, crmService, manyToManyMetadata.Entity2LogicalName, entity2);

                // Associate entities.
                AssociateEntities(crmService, relationshipname, manyToManyMetadata.Entity1LogicalName, entity1id, manyToManyMetadata.Entity2LogicalName, entity2id, associationType);
            }
            catch (SoapException se)
            {
                StringBuilder builder = new StringBuilder();
                XmlNode code = se.Detail.SelectSingleNode("//code");
                if (code != null)
                {
                    // ignore duplicate key error and add remark text.
                    if (code.InnerText == "0x80040237")
                    {
                        if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_remarks))
                        {
                            entity.Properties.Add(new StringProperty(ManyToManyRelationshipPlugin.Np_remarks, "ignore duplicate key."));
                        }
                        else
                        {
                            string remark = entity.Properties[ManyToManyRelationshipPlugin.Np_remarks] as string;
                            entity.Properties[ManyToManyRelationshipPlugin.Np_remarks] = remark + "ignore duplicate key.";
                        }
                        return;
                    }
                    else
                    {
                        builder.Append("Error Code:" + code.InnerText);
                    }
                }
                XmlNode description = se.Detail.SelectSingleNode("//description");
                if (description != null)
                {
                    builder.Append("Error Message:" + description.InnerText);
                }
                ThrowException(builder.ToString(), se);
            }
        }
        /// <summary>
        /// Associate or disassociate entity records from arguments information.
        /// </summary>
        /// <param name="crmService">CrmService Instance</param>
        /// <param name="relationshipname">Many to Many relationship name</param>
        /// <param name="entity1LogicalName">logical name of entity1</param>
        /// <param name="entity1id">guid of entity1 record</param>
        /// <param name="entity2LogicalName">logical name of entity2</param>
        /// <param name="entity2id">guid of entity2 record</param>
        /// <param name="associationType">association type</param>
        private void AssociateEntities(ICrmService crmService, string relationshipname, string entity1LogicalName, Guid entity1id, string entity2LogicalName, Guid entity2id, Picklist associationType)
        {
            Moniker moniker1 = new Moniker(entity1LogicalName, entity1id);
            Moniker moniker2 = new Moniker(entity2LogicalName, entity2id);

            Request request = null;
            if (associationType.Value == Disassociate)
            {
                // Disassociate Entities
                DisassociateEntitiesRequest disassociateEntitiesRequest = new DisassociateEntitiesRequest();
                disassociateEntitiesRequest.Moniker1 = moniker1;
                disassociateEntitiesRequest.Moniker2 = moniker2;
                disassociateEntitiesRequest.RelationshipName = relationshipname;
                request = disassociateEntitiesRequest;
            }
            else
            {
                // Associate Entities
                AssociateEntitiesRequest associateEntitiesRequest = new AssociateEntitiesRequest();
                associateEntitiesRequest.Moniker1 = moniker1;
                associateEntitiesRequest.Moniker2 = moniker2;
                associateEntitiesRequest.RelationshipName = relationshipname;
                request = associateEntitiesRequest;
            }

            crmService.Execute(request);
        }
        /// <summary>
        /// Retrieve Metadat of Many to Many relashonship.
        /// </summary>
        /// <param name="metaService">MetadataService instance</param>
        /// <param name="relationshipName">Name of Many to Many relashonship to Retrieve</param>
        /// <returns>ManyToManyMetadata</returns>
        private ManyToManyMetadata GetManyToManyMetadata(IMetadataService metaService, string relationshipName)
        {
            RetrieveRelationshipRequest request = new RetrieveRelationshipRequest();
            request.Name = relationshipName;
            // retrieve currently published relationship
            request.RetrieveAsIfPublished = false;
            RetrieveRelationshipResponse response = metaService.Execute(request) as RetrieveRelationshipResponse;

            ManyToManyMetadata metadata = response.RelationshipMetadata as ManyToManyMetadata;
            if (metadata == null)
            {
                ThrowException("relationshipname is not many to many relationsihpname");
            }
            return metadata;
        }
        /// <summary>
        /// Retrieve Entity Metadata from enttyLogicalName of arguemnts.
        /// </summary>
        /// <param name="metadataService">MetadatService instance</param>
        /// <param name="entityLogicalName">entity logical name to Retrieve</param>
        /// <returns>EntityMetadata</returns>
        private EntityMetadata GetEntityMetadata(IMetadataService metadataService, string entityLogicalName)
        {
            // Retrieve Primary Field
            RetrieveEntityRequest request = new RetrieveEntityRequest();
            request.LogicalName = entityLogicalName;
            request.RetrieveAsIfPublished = false;
            request.EntityItems = EntityItems.EntityOnly;

            RetrieveEntityResponse response = metadataService.Execute(request) as RetrieveEntityResponse;

            return response.EntityMetadata;

        }
        /// <summary>
        /// Retrieve entity record guid using entityLogicalName and PrimaryFieldValue of arguements.
        /// retrieve entity records with criteria that entity primary fields match primaryfieldvalue of argument. 
        /// if no record is found or more than 1 record is found then it throw exception.
        /// </summary>
        /// <param name="metadataService">MetadataService intance</param>
        /// <param name="crmService">CrmServie instance</param>
        /// <param name="entityLogicalName">entity logical name to retrieve record</param>
        /// <param name="primaryFieldValue">entty primary filed value for filter records</param>
        /// <returns></returns>
        private Guid GetEntityId(IMetadataService metadataService, ICrmService crmService, string entityLogicalName, string primaryFieldValue)
        {
            EntityMetadata entityMetadata = GetEntityMetadata(metadataService, entityLogicalName);
            string primaryField = entityMetadata.PrimaryField;
            string primaryKey = entityMetadata.PrimaryKey;

            // Retrieve Entity Record Id
            QueryExpression query = new QueryExpression(entityLogicalName);
            query.ColumnSet = new ColumnSet();
            ConditionExpression condition = new ConditionExpression(primaryField, ConditionOperator.Equal, primaryFieldValue);
            query.Criteria.AddCondition(condition);

            RetrieveMultipleRequest request = new RetrieveMultipleRequest();
            request.ReturnDynamicEntities = true;
            request.Query = query;
            RetrieveMultipleResponse retrieveResponse = crmService.Execute(request) as RetrieveMultipleResponse;
            BusinessEntityCollection entities = retrieveResponse.BusinessEntityCollection;
            if (entities.BusinessEntities.Count != 1)
            {
                ThrowException("0 or many entity record is found.");
            }
            return ((entities.BusinessEntities[0] as DynamicEntity).Properties[primaryKey] as Key).Value;
        }
        /// <summary>
        /// Validate Plugin Execution Context.Check plugin message to trigger plugin.
        /// Check Required Fields is contained in InputParameters
        /// </summary>
        /// <param name="context"></param>
        private void ValidateContext(IPluginExecutionContext context)
        {
            // check message and entity
            if (context.MessageName != MessageName.Create)
            {
                ThrowException("unsupported message");
            }
            if (!context.PrimaryEntityName.Equals("np_manytomanyrelationship"))
            {
                ThrowException("unsupported entity");
            }
            // check if attribute is exists
            DynamicEntity entity = context.InputParameters[ParameterName.Target] as DynamicEntity;
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_relationshipname))
            {
                ThrowException("relationshipname is required");
            }
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_entity1))
            {
                ThrowException("entity1 is required ");
            }
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_entity2))
            {
                ThrowException("entity2 is required ");
            }
            if (!entity.Properties.Contains(ManyToManyRelationshipPlugin.Np_associationtype))
            {
                ThrowException("association type is required ");
            }
        }
        #region helper method
        /// <summary>
        /// Helper Method to Generate Exception.
        /// </summary>
        /// <param name="message">error message</param>
        /// <param name="innerException">original exception</param>
        private void ThrowException(string message, Exception innerException)
        {
            throw new InvalidPluginExecutionException(message, innerException);
        }
        /// <summary>
        /// Helper Method to Generate Exception
        /// </summary>
        /// <param name="message">error message</param>
        private void ThrowException(string message)
        {
            ThrowException(message, null);
        }
        #endregion

    }
}
